/**
 * Copyright 2016 Netflix, Inc.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.hystrix;

import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;

/**
 * Immutable holder class for the status of command execution.
 * <p>
 * This object can be referenced and "modified" by parent and child threads as well as by different instances of HystrixCommand since
 * 1 instance could create an ExecutionResult, cache a Future that refers to it, a 2nd instance execution then retrieves a Future
 * from cache and wants to append RESPONSE_FROM_CACHE to whatever the ExecutionResult was from the first command execution.
 * <p>
 * This being immutable forces and ensure thread-safety instead of using AtomicInteger/ConcurrentLinkedQueue and determining
 * when it's safe to mutate the object directly versus needing to deep-copy clone to a new instance.
 */
public class ExecutionResult
{
    private final EventCounts eventCounts;
    private final Exception failedExecutionException;
    private final Exception executionException;
    private final long startTimestamp;
    private final int executionLatency; //time spent in run() method
    private final int userThreadLatency; //time elapsed between caller thread submitting request and response being visible to it
    private final boolean executionOccurred;
    private final boolean isExecutedInThread;
    private final HystrixCollapserKey collapserKey;

    private static final HystrixEventType[] ALL_EVENT_TYPES = HystrixEventType.values();
    private static final int NUM_EVENT_TYPES = ALL_EVENT_TYPES.length;
    private static final BitSet EXCEPTION_PRODUCING_EVENTS = new BitSet(NUM_EVENT_TYPES);
    private static final BitSet TERMINAL_EVENTS = new BitSet(NUM_EVENT_TYPES);

    static
    {
        for (HystrixEventType eventType : HystrixEventType.EXCEPTION_PRODUCING_EVENT_TYPES)
        {
            EXCEPTION_PRODUCING_EVENTS.set(eventType.ordinal());
        }

        for (HystrixEventType eventType : HystrixEventType.TERMINAL_EVENT_TYPES)
        {
            TERMINAL_EVENTS.set(eventType.ordinal());
        }
    }

    public static class EventCounts
    {
        private final BitSet events;
        private final int numEmissions;
        private final int numFallbackEmissions;
        private final int numCollapsed;

        EventCounts()
        {
            this.events = new BitSet(NUM_EVENT_TYPES);
            this.numEmissions = 0;
            this.numFallbackEmissions = 0;
            this.numCollapsed = 0;
        }

        EventCounts(BitSet events, int numEmissions, int numFallbackEmissions, int numCollapsed)
        {
            this.events = events;
            this.numEmissions = numEmissions;
            this.numFallbackEmissions = numFallbackEmissions;
            this.numCollapsed = numCollapsed;
        }

        EventCounts(HystrixEventType... eventTypes)
        {
            BitSet newBitSet = new BitSet(NUM_EVENT_TYPES);
            int localNumEmits = 0;
            int localNumFallbackEmits = 0;
            int localNumCollapsed = 0;
            for (HystrixEventType eventType : eventTypes)
            {
                switch (eventType)
                {
                    case EMIT:
                        newBitSet.set(HystrixEventType.EMIT.ordinal());
                        localNumEmits++;
                        break;
                    case FALLBACK_EMIT:
                        newBitSet.set(HystrixEventType.FALLBACK_EMIT.ordinal());
                        localNumFallbackEmits++;
                        break;
                    case COLLAPSED:
                        newBitSet.set(HystrixEventType.COLLAPSED.ordinal());
                        localNumCollapsed++;
                        break;
                    default:
                        newBitSet.set(eventType.ordinal());
                        break;
                }
            }
            this.events = newBitSet;
            this.numEmissions = localNumEmits;
            this.numFallbackEmissions = localNumFallbackEmits;
            this.numCollapsed = localNumCollapsed;
        }

        EventCounts plus(HystrixEventType eventType)
        {
            return plus(eventType, 1);
        }

        EventCounts plus(HystrixEventType eventType, int count)
        {
            BitSet newBitSet = (BitSet) events.clone();
            int localNumEmits = numEmissions;
            int localNumFallbackEmits = numFallbackEmissions;
            int localNumCollapsed = numCollapsed;
            switch (eventType)
            {
                case EMIT:
                    newBitSet.set(HystrixEventType.EMIT.ordinal());
                    localNumEmits += count;
                    break;
                case FALLBACK_EMIT:
                    newBitSet.set(HystrixEventType.FALLBACK_EMIT.ordinal());
                    localNumFallbackEmits += count;
                    break;
                case COLLAPSED:
                    newBitSet.set(HystrixEventType.COLLAPSED.ordinal());
                    localNumCollapsed += count;
                    break;
                default:
                    newBitSet.set(eventType.ordinal());
                    break;
            }
            return new EventCounts(newBitSet, localNumEmits, localNumFallbackEmits, localNumCollapsed);
        }

        public boolean contains(HystrixEventType eventType)
        {
            return events.get(eventType.ordinal());
        }

        public boolean containsAnyOf(BitSet other)
        {
            return events.intersects(other);
        }

        public int getCount(HystrixEventType eventType)
        {
            switch (eventType)
            {
                case EMIT:
                    return numEmissions;
                case FALLBACK_EMIT:
                    return numFallbackEmissions;
                case EXCEPTION_THROWN:
                    return containsAnyOf(EXCEPTION_PRODUCING_EVENTS) ? 1 : 0;
                case COLLAPSED:
                    return numCollapsed;
                default:
                    return contains(eventType) ? 1 : 0;
            }
        }

        @Override
        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            EventCounts that = (EventCounts) o;

            if (numEmissions != that.numEmissions) return false;
            if (numFallbackEmissions != that.numFallbackEmissions) return false;
            if (numCollapsed != that.numCollapsed) return false;
            return events.equals(that.events);

        }

        @Override
        public int hashCode()
        {
            int result = events.hashCode();
            result = 31 * result + numEmissions;
            result = 31 * result + numFallbackEmissions;
            result = 31 * result + numCollapsed;
            return result;
        }

        @Override
        public String toString()
        {
            return "EventCounts{" +
                    "events=" + events +
                    ", numEmissions=" + numEmissions +
                    ", numFallbackEmissions=" + numFallbackEmissions +
                    ", numCollapsed=" + numCollapsed +
                    '}';
        }
    }

    private ExecutionResult(EventCounts eventCounts, long startTimestamp, int executionLatency,
                            int userThreadLatency, Exception failedExecutionException, Exception executionException,
                            boolean executionOccurred, boolean isExecutedInThread, HystrixCollapserKey collapserKey)
    {
        this.eventCounts = eventCounts;
        this.startTimestamp = startTimestamp;
        this.executionLatency = executionLatency;
        this.userThreadLatency = userThreadLatency;
        this.failedExecutionException = failedExecutionException;
        this.executionException = executionException;
        this.executionOccurred = executionOccurred;
        this.isExecutedInThread = isExecutedInThread;
        this.collapserKey = collapserKey;
    }

    // we can return a static version since it's immutable
    static ExecutionResult EMPTY = ExecutionResult.from();

    public static ExecutionResult from(HystrixEventType... eventTypes)
    {
        boolean didExecutionOccur = false;
        for (HystrixEventType eventType : eventTypes)
        {
            if (didExecutionOccur(eventType))
            {
                didExecutionOccur = true;
            }
        }
        return new ExecutionResult(new EventCounts(eventTypes), -1L, -1, -1, null, null, didExecutionOccur, false, null);
    }

    private static boolean didExecutionOccur(HystrixEventType eventType)
    {
        switch (eventType)
        {
            case SUCCESS:
                return true;
            case FAILURE:
                return true;
            case BAD_REQUEST:
                return true;
            case TIMEOUT:
                return true;
            case CANCELLED:
                return true;
            default:
                return false;
        }
    }

    public ExecutionResult setExecutionOccurred()
    {
        return new ExecutionResult(eventCounts, startTimestamp, executionLatency, userThreadLatency,
                failedExecutionException, executionException, true, isExecutedInThread, collapserKey);
    }

    public ExecutionResult setExecutionLatency(int executionLatency)
    {
        return new ExecutionResult(eventCounts, startTimestamp, executionLatency, userThreadLatency,
                failedExecutionException, executionException, executionOccurred, isExecutedInThread, collapserKey);
    }

    public ExecutionResult setException(Exception e)
    {
        return new ExecutionResult(eventCounts, startTimestamp, executionLatency, userThreadLatency, e,
                executionException, executionOccurred, isExecutedInThread, collapserKey);
    }

    public ExecutionResult setExecutionException(Exception executionException)
    {
        return new ExecutionResult(eventCounts, startTimestamp, executionLatency, userThreadLatency,
                failedExecutionException, executionException, executionOccurred, isExecutedInThread, collapserKey);
    }

    public ExecutionResult setInvocationStartTime(long startTimestamp)
    {
        return new ExecutionResult(eventCounts, startTimestamp, executionLatency, userThreadLatency,
                failedExecutionException, executionException, executionOccurred, isExecutedInThread, collapserKey);
    }

    public ExecutionResult setExecutedInThread()
    {
        return new ExecutionResult(eventCounts, startTimestamp, executionLatency, userThreadLatency,
                failedExecutionException, executionException, executionOccurred, true, collapserKey);
    }

    public ExecutionResult setNotExecutedInThread()
    {
        return new ExecutionResult(eventCounts, startTimestamp, executionLatency, userThreadLatency,
                failedExecutionException, executionException, executionOccurred, false, collapserKey);
    }

    public ExecutionResult markCollapsed(HystrixCollapserKey collapserKey, int sizeOfBatch)
    {
        return new ExecutionResult(eventCounts.plus(HystrixEventType.COLLAPSED, sizeOfBatch), startTimestamp, executionLatency, userThreadLatency,
                failedExecutionException, executionException, executionOccurred, isExecutedInThread, collapserKey);
    }

    public ExecutionResult markUserThreadCompletion(long userThreadLatency)
    {
        if (startTimestamp > 0 && !isResponseRejected())
        {
            /* execution time (must occur before terminal state otherwise a race condition can occur if requested by client) */
            return new ExecutionResult(eventCounts, startTimestamp, executionLatency, (int) userThreadLatency,
                    failedExecutionException, executionException, executionOccurred, isExecutedInThread, collapserKey);
        } else
        {
            return this;
        }
    }

    /**
     * Creates a new ExecutionResult by adding the defined 'event' to the ones on the current instance.
     *
     * @param eventType event to add
     * @return new {@link ExecutionResult} with event added
     */
    public ExecutionResult addEvent(HystrixEventType eventType)
    {
        return new ExecutionResult(eventCounts.plus(eventType), startTimestamp, executionLatency,
                userThreadLatency, failedExecutionException, executionException,
                executionOccurred, isExecutedInThread, collapserKey);
    }

    public ExecutionResult addEvent(int executionLatency, HystrixEventType eventType)
    {
        if (startTimestamp >= 0 && !isResponseRejected())
        {
            return new ExecutionResult(eventCounts.plus(eventType), startTimestamp, executionLatency,
                    userThreadLatency, failedExecutionException, executionException,
                    executionOccurred, isExecutedInThread, collapserKey);
        } else
        {
            return addEvent(eventType);
        }
    }

    public EventCounts getEventCounts()
    {
        return eventCounts;
    }

    public long getStartTimestamp()
    {
        return startTimestamp;
    }

    public int getExecutionLatency()
    {
        return executionLatency;
    }

    public int getUserThreadLatency()
    {
        return userThreadLatency;
    }

    public long getCommandRunStartTimeInNanos()
    {
        return startTimestamp * 1000 * 1000;
    }

    public Exception getException()
    {
        return failedExecutionException;
    }

    public Exception getExecutionException()
    {
        return executionException;
    }

    public HystrixCollapserKey getCollapserKey()
    {
        return collapserKey;
    }

    public boolean isResponseSemaphoreRejected()
    {
        return eventCounts.contains(HystrixEventType.SEMAPHORE_REJECTED);
    }

    public boolean isResponseThreadPoolRejected()
    {
        return eventCounts.contains(HystrixEventType.THREAD_POOL_REJECTED);
    }

    public boolean isResponseRejected()
    {
        return isResponseThreadPoolRejected() || isResponseSemaphoreRejected();
    }

    public List<HystrixEventType> getOrderedList()
    {
        List<HystrixEventType> eventList = new ArrayList<HystrixEventType>();
        for (HystrixEventType eventType : ALL_EVENT_TYPES)
        {
            if (eventCounts.contains(eventType))
            {
                eventList.add(eventType);
            }
        }
        return eventList;
    }

    public boolean isExecutedInThread()
    {
        return isExecutedInThread;
    }

    public boolean executionOccurred()
    {
        return executionOccurred;
    }

    public boolean containsTerminalEvent()
    {
        return eventCounts.containsAnyOf(TERMINAL_EVENTS);
    }

    @Override
    public String toString()
    {
        return "ExecutionResult{" +
                "eventCounts=" + eventCounts +
                ", failedExecutionException=" + failedExecutionException +
                ", executionException=" + executionException +
                ", startTimestamp=" + startTimestamp +
                ", executionLatency=" + executionLatency +
                ", userThreadLatency=" + userThreadLatency +
                ", executionOccurred=" + executionOccurred +
                ", isExecutedInThread=" + isExecutedInThread +
                ", collapserKey=" + collapserKey +
                '}';
    }
}
